package hydrap2p.download;

import hydrap2p.ServiceLocation;
import hydrap2p.download.strategy.BTStrategy;
import hydrap2p.download.strategy.BasicStrategy;
import hydrap2p.helper.ServiceConnector;
import hydrap2p.helper.ThriftServerThread;
import hydrap2p.helper.ThriftUtils;
import hydrap2p.helper.Utility;
import hydrap2p.library.FileAlreadyExists;
import hydrap2p.library.IOProblem;
import hydrap2p.library.InvalidUID;
import hydrap2p.library.Library;
import hydrap2p.library.Range;
import hydrap2p.logging.Level;
import hydrap2p.logging.Logging;
import hydrap2p.modulemanager.ModuleControl;
import hydrap2p.servicemanager.NameTaken;

import java.io.File;
import java.io.IOException;
import java.net.BindException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import java.util.Map.Entry;

import org.apache.log4j.PropertyConfigurator;
import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
import org.apache.thrift.transport.TTransportException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;

/**
 * The Class DownloaderImpl.
 */
public class DownloaderImpl implements Downloader.Iface, ModuleControl.Iface {
	// Init logger
	static {
		//Configure log4j
		Properties log4jProps = new Properties();
		log4jProps.setProperty("log4j.rootLogger", "TRACE, console");
		log4jProps.setProperty("log4j.appender.console", "org.apache.log4j.ConsoleAppender");
		log4jProps.setProperty("log4j.appender.console.layout", "org.apache.log4j.PatternLayout");
		log4jProps.setProperty("log4j.appender.console.layout.ConversionPattern", "%-4r [%t] %-5p %c %x - %m%n");

		PropertyConfigurator.configure(log4jProps);
	}

	// Class string name, doubling as logging marker
	private static final Marker TAG = MarkerFactory.getDetachedMarker("Downloader");

	// Console logger - used before connections established
	private static final Logger consoleLogger = LoggerFactory.getLogger(TAG.getName());
	
	//Download chunk size
	public static final int CHUNK_SIZE = 1024 * 256;

	// Internal data structures
	private Map<Long, Download> downloads;
	private Map<String, ProtocolAdapter> protocols;
	private List<DownloadSubscriber> subscribers;	//List of subscribers interested in everything
	
	//Event forwarder
	private DownloaderEventForwarder eventFwd;
	
	private static ThriftServerThread t;
	private static ThriftServerThread control;
	
	//Persistence file
	private static final String SAVED_FILENAME = "downloaderData.db";
	private static File savedDownloads = new File(Utility.appendPath(Utility.getUserConfigurationPath(), SAVED_FILENAME));

	// Port that we're running on
	private int port;
	
	/**
	 * Instantiates a new downloader impl.
	 * 
	 * @param port the port
	 */
	public DownloaderImpl(int port) {
		this.port = port;
		this.downloads = Collections.synchronizedMap(new TreeMap<Long, Download>());
		this.protocols = Collections.synchronizedMap(new TreeMap<String, ProtocolAdapter>());
		this.subscribers = Collections.synchronizedList(new ArrayList<DownloadSubscriber>());
		this.eventFwd = new DownloaderEventForwarder();
	}

	/**
	 * The main method.
	 * 
	 * @param args the arguments
	 */
	public static void main(String[] args) {
		DownloaderImpl downloader = null;
		int port = 23205;
		
		try {
			downloader = new DownloaderImpl(port);
		} catch(Exception e) {
			System.out.println("Failed to start Downloader, " + e.getMessage());
			return;
		}

		//Load state
		consoleLogger.debug("Loading saved state...");
		downloader.downloads = DownloaderPersistence.loadAllDownloads(savedDownloads);
		
		// Start server
		try {
			t = ThriftUtils.initThriftServer(new Downloader.Processor(downloader), "Downloader", port);
			control = ThriftUtils.initThriftServer(new ModuleControl.Processor(downloader), "Downloader.Control");
		} catch (BindException e) {
			consoleLogger.error(TAG, "Failed to start Downloader!  Could not bind to port.");
			e.printStackTrace();
			return;
		} catch (IOException e) {
			consoleLogger.error(TAG, "Failed to start Downloader!");
			e.printStackTrace();
			return;
		} catch (NameTaken e) {
			consoleLogger.error(TAG, "Failed to start Downloader!  Duplicate module exists.");
			e.printStackTrace();
			return;
		}
		
		//Ensure module connections
		boolean manager = ThriftUtils.waitForManager();
		boolean library = ThriftUtils.waitForLibrary();
		
		if(manager && library) {
			t.start();
			control.start();
		}
		else
			consoleLogger.error(TAG, "Failed to connect to essential modules!  Exiting.");
		boolean logging = ThriftUtils.waitForLogging();
	}
	
	/* (non-Javadoc)
	 * @see hydrap2p.download.Downloader.Iface#addDownloadProtocol(hydrap2p.ServiceLocation, java.lang.String)
	 */
	@Override
	public boolean addDownloadProtocol(ServiceLocation location, String type) throws TException {
		// If we don't know about this protocol, add the service to the list.
		// Otherwise, fail
		if(getProtocolByType(type) == null) {
			log(Level.DEBUG, "Added protocol type " + type + " at " + location.getAddress() + ":" + location.getPort());
		} else {	
			log(Level.DEBUG, "Replacing protocol type " + type + " at " + location.getAddress() + ":" + location.getPort());
		}
		
		try {
			ProtocolAdapter adapter = new ProtocolAdapter(location, type);
			protocols.put(type, adapter);
		} catch(TTransportException e) {
			return false;
		}
		
		//Restart downloads for this protocol
		for(Entry<Long, Download> entry : downloads.entrySet()) {
			for(Source s : entry.getValue().getSources()) {
				if(s.getProtocol().equals(type)) {
					startDownloadChunkThread(entry.getKey(), s);
				}
			}
		}
		
		return true;
	}

	/* (non-Javadoc)
	 * @see hydrap2p.download.Downloader.Iface#addSource(long, hydrap2p.download.Source)
	 */
	@Override
	public boolean addSource(long uid, Source source) throws TException {
		if(source == null)
			return false;
		
		boolean result = false;
		Download d = getImplDownloadWithUid(uid);
		
		if(d != null) {
			result = d.addSource(source);

			if(result) {
				log(Level.DEBUG, "Added source to download uid: " + uid);
	
				//If this is the first source for the download, start it
				if(d.getSources().size() == 1)
					startDownloadChunkThread(uid, source);
				
				//Inform listeners of event
				forwardEventNewSource(d, source);
			} else {
				log(Level.DEBUG, "Failed to add source to download uid: " + uid);
			}
		} 

		return result;
	}

	/* (non-Javadoc)
	 * @see hydrap2p.download.Downloader.Iface#createDownload(java.util.List, java.lang.String, java.lang.String, long)
	 */
	@Override
	public long createDownload(List<Source> sources, String downloadDir, String filename, long size) throws TException {
		if(size < 0)
			return actuallyCreateDownload(sources, downloadDir, new ArrayList<String>(), new ArrayList<Long>(), false);
		else
			return actuallyCreateDownload(sources, downloadDir, Arrays.asList(filename), Arrays.asList(size), false);
	}

	/* (non-Javadoc)
	 * @see hydrap2p.download.Downloader.Iface#createMultifileDownload(java.util.List, java.lang.String, java.util.List, java.util.List)
	 */
	@Override
	public long createMultifileDownload(List<Source> sources, String downloadDir, List<String> files, List<Long> sizes) throws TException {
		return actuallyCreateDownload(sources, downloadDir, files, sizes, true);
	}
	
	/**
	 * Actually create download.
	 * 
	 * @param sources the sources
	 * @param downloadDir the download dir
	 * @param files the files
	 * @param sizes the sizes
	 * @param multifile the multifile
	 * @return the long
	 * @throws TException the t exception
	 */
	private long actuallyCreateDownload(List<Source> sources, String downloadDir, List<String> files, List<Long> sizes, boolean multifile) throws TException {
		Download d = new Download(sources, downloadDir, files, sizes, DownloadPriority.NORMAL, DownloadState.ACTIVE);
		
		//TODO: Verify that this usage is correct with Walter
		//Inform library
		List<Long> fileUids = new ArrayList<Long>();
		long filesetUid = -1;
		
		try{
			Library.Client library = ServiceConnector.getLibrary();
			
			if(files.size() != 0) {
				if(multifile) {
					//Create all files requested
					for(int x = 0;x < files.size();x++)
						fileUids.add(library.createFile(Utility.appendPath(downloadDir, files.get(x)), sizes.get(x)));
					
					//Create fileset, use that uid as the download's.  Using downloadDir as the set name until I think of something better
					filesetUid = library.createFileSet(downloadDir, fileUids);
				} else {
					filesetUid = library.createFile(Utility.appendPath(downloadDir, files.get(0)), sizes.get(0));
				}
			} else {
				//TODO: Reimplement this once Library attachments have been made to Snark
//				filesetUid = library.createFile(null, 0);
			}
		} catch(TException e) {
			//Big problem, library connection crapped out mid-creation
			log(Level.ERROR, "Lost connection to library creating download!");
			throw e;
		} catch(FileAlreadyExists e) {
			//TODO: Decide how to handle this and below.  Until then, log it, stacktrace and terminate
			log(Level.ERROR, "FileAlreadyExists exception caught, not creating download");
			e.printStackTrace();
			return -1;
		} catch(IOProblem e) {
			//TODO: Decide how to handle this and below.  Until then, log it, stacktrace and terminate
			log(Level.ERROR, "IOProblem exception caught, not creating download");
			e.printStackTrace();
			return -1;
		} catch (InvalidUID e) {
			//TODO: Decide how to handle this and below.  Until then, log it, stacktrace and terminate
			log(Level.ERROR, "InvalidUID exception caught, not creating download");
			e.printStackTrace();
			return -1;
		}
		
		d.setUid(filesetUid);
		
		log(Level.DEBUG, "Created new download with " + files.size() + " files in " + downloadDir);
		
		log(Level.DEBUG, "Recalculating download ranges...");
		//Init download sources
		d.recalculateAvailableRanges();
		log(Level.DEBUG, "Done!");
		
		downloads.put(d.getUid(), d);
		log(Level.DEBUG, "Available ranges:");
		
		for(Range r : d.getAvailable())
			log(Level.DEBUG, "Start: " + r.start + " Len: " + r.len);
		
		//If the download has sources, start it
		for(int x = 0;x < d.getSources().size();x++) {
			log(Level.DEBUG, "Starting download with source " + d.getSources().get(x).ip + ":" + d.getSources().get(x).port);
			
			startDownloadChunkThread(d.getUid(), d.getSources().get(x));
		}
		
		//Inform listeners of event
		forwardEventNewDownload(d.getUid());
		
		return filesetUid;
	}

	/* (non-Javadoc)
	 * @see hydrap2p.download.Downloader.Iface#deleteSourceMapExtra(long, hydrap2p.download.Source, java.lang.String)
	 */
	@Override
	public void deleteSourceMapExtra(long uid, Source source, String key) throws TException {
		if(source == null)
			return;
		
		Download d = getImplDownloadWithUid(uid);
		if(d != null) {
			List<Source> sources = d.getSources();
			int index = sources.indexOf(source);

			if(index >= 0) {
				Source s = sources.get(index);
				s.extra.remove(key);
				
				log(Level.DEBUG, "Removed source extra from download uid: " + uid + " with key " + key);
				
				//Inform listeners of event
				forwardEventUpdateSource(d.getUid(), s);
			}
		}
	}

	/* (non-Javadoc)
	 * @see hydrap2p.download.Downloader.Iface#erroredDownloading(long, hydrap2p.download.Source, hydrap2p.library.Range, boolean)
	 */
	@Override
	public void erroredDownloading(final long uid, final Source source, Range range, boolean permanent) throws TException {
		if(source != null) {
			if (permanent)
				log(Level.MISHAP, "Permanent error downloading from " + source.ip + ":" + source.port);
			else
				log(Level.MISHAP, "Temporary error downloading from " + source.ip + ":" + source.port);
			
			Download d = getImplDownloadWithUid(uid);
			// If permanent error, remove the source, else re-add the range to available and try again
			if(permanent) {
				if(d != null) {
					d.getSources().remove(source);
					d.addAvailableRange(range);
				}
			} else {
				if(d != null)  {
					d.addAvailableRange(range);
					
					startDownloadChunkThread(uid, source);
				}
			}
		} else
			log(Level.ERROR, "Null source!  This probably shouldn't ever happen.");
	}

	/* (non-Javadoc)
	 * @see hydrap2p.download.Downloader.Iface#finishedDownloading(long, hydrap2p.download.Source, hydrap2p.library.Range)
	 */
	@Override
	public void finishedDownloading(final long uid, final Source source, Range range) throws TException {
		if(source == null) {
			erroredDownloading(uid, null, range, false);
			return;
		}
		
		log(Level.INFO, "Finished downloading " + range.len / 1024 + "kb from " + source.ip + ":" + source.port);
		
		Download d = getImplDownloadWithUid(uid);
		
		if(d != null) {
			log(Level.DEBUG, "Adding downloaded range...");
			//Mark as downloaded
			d.addDownloadedRange(range);
			log(Level.DEBUG, "Done!");
			
			startDownloadChunkThread(uid, source);
			
			//Forward event
			forwardEventUpdateDownloadProgress(uid, source, range.len, d.getTotalDownloaded());	
		}
	}
	
	/**
	 * Start download chunk thread.
	 * 
	 * @param uid the uid
	 * @param source the source
	 */
	private void startDownloadChunkThread(final long uid, final Source source) {
		//Check for download active state to continue
		Download d = getImplDownloadWithUid(uid);
		if(d.getState() != DownloadState.ACTIVE) {
			return;
		}
		
		//Select next download piece
		Thread t = new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					if(source.getProtocol().equalsIgnoreCase("bittorrent"))
						new BTStrategy().downloadNextChunk(DownloaderImpl.this, uid, source);
					else
						new BasicStrategy(CHUNK_SIZE).downloadNextChunk(DownloaderImpl.this, uid, source);
				} catch (TException e) {
					e.printStackTrace();
				}
			}
		});
		
		t.start();
	}
	
	/* (non-Javadoc)
	 * @see hydrap2p.download.Downloader.Iface#getDownloadPriority(long)
	 */
	@Override
	public DownloadPriority getDownloadPriority(long uid) throws TException {
		Download d = getImplDownloadWithUid(uid);
		if(d != null)
			return d.getPriority();
		else
			return null;
	}

	/* (non-Javadoc)
	 * @see hydrap2p.download.Downloader.Iface#getDownloadProtocols()
	 */
	@Override
	public List<Protocol> getDownloadProtocols() throws TException {
		List<Protocol> retList = new ArrayList<Protocol>();
		
		for(Entry<String, ProtocolAdapter> entry : protocols.entrySet())
			retList.add(new Protocol(entry.getValue().getLocation(), entry.getKey()));
		
		return retList;
	}

	/* (non-Javadoc)
	 * @see hydrap2p.download.Downloader.Iface#getDownloadState(long)
	 */
	@Override
	public DownloadState getDownloadState(long uid) throws TException {
		Download d = getImplDownloadWithUid(uid);
		if(d != null)
			return d.getState();
		else
			return null;
	}

	/* (non-Javadoc)
	 * @see hydrap2p.download.Downloader.Iface#listSources(long)
	 */
	@Override
	public List<Source> listSources(long uid) throws TException {
		Download d = getImplDownloadWithUid(uid);
		if(d != null)
			return d.getSources();
		else
			return null;
	}

	/* (non-Javadoc)
	 * @see hydrap2p.download.Downloader.Iface#setDownloadState(long, hydrap2p.download.State)
	 */
	@Override
	public void setDownloadState(long uid, DownloadState state) throws TException {
		Download d = getImplDownloadWithUid(uid);
		if(d != null) {
			DownloadState oldState = d.getState();
			if(oldState != state) {
				d.setState(state);
				
				log(Level.DEBUG, "Setting download uid " + uid + " to state " + state);
				
				//Inform listeners of event
				forwardEventUpdateDownloadState(d.getUid(), state);
				
				//Restart a stopped/paused download
				if(state == DownloadState.ACTIVE) {
					for(Source s : d.getSources()) {
						startDownloadChunkThread(uid, s);
					}
				}
			}
		}
	}

	/* (non-Javadoc)
	 * @see hydrap2p.download.Downloader.Iface#removeDownloadProtocol(hydrap2p.ServiceLocation)
	 */
	@Override
	public boolean removeDownloadProtocol(ServiceLocation location) throws TException {
		if(location == null)
			return false;

		Protocol protocol = new Protocol();
		protocol.setLocation(location);
		
		boolean result = false;
		
		for(Entry<String, ProtocolAdapter> entry : protocols.entrySet()) {
			if(entry.getValue().getLocation().equals(location)) {
				result = true;
				protocols.remove(entry.getKey());
				
				break;
			}
		}
		
		if(result)
			log(Level.DEBUG, "Removed protocol at " + location.getAddress() + ":" + location.getPort());
		else
			log(Level.DEBUG, "Failed to remove protocol at " + location.getAddress() + ":" + location.getPort());
		
		return result;
	}

	/* (non-Javadoc)
	 * @see hydrap2p.download.Downloader.Iface#removeSource(long, hydrap2p.download.Source)
	 */
	@Override
	public boolean removeSource(long uid, Source source) throws TException {
		if(source == null)
			return false;
		
		Download d = getImplDownloadWithUid(uid);
		boolean result = false;
		if(d != null) {
			result = d.removeSource(source);
		
			if(result)
				log(Level.DEBUG, "Removed source from uid " + uid);
			else
				log(Level.DEBUG, "Failed to remove source from uid " + uid);
			
			//Inform listeners of event
			forwardEventRemovedSource(d.getUid(), source);
		}
		
		return result;
	}

	/* (non-Javadoc)
	 * @see hydrap2p.download.Downloader.Iface#setDownloadPriority(long, hydrap2p.download.Priority)
	 */
	@Override
	public void setDownloadPriority(long uid, DownloadPriority priority) throws TException {
		Download d = getImplDownloadWithUid(uid);
		if(d != null) {
			d.setPriority(priority);
			
			log(Level.DEBUG, "Setting priority of uid " + uid + " to " + priority);
		}
	}

	/* (non-Javadoc)
	 * @see hydrap2p.download.Downloader.Iface#setSourceAvailableRanges(long, hydrap2p.download.Source, java.util.List)
	 */
	@Override
	public void setSourceAvailableRanges(long uid, Source source, List<Range> ranges) throws TException {
		if(source == null || ranges == null)
			return;
		
		Download d = getImplDownloadWithUid(uid);
		
		if(d != null) {
			Source s = getSource(uid, source);

			if(s != null) {
				s.setAvailable(ranges);
				for(Range r : ranges)
					d.addAvailableRange(r);
				
				//Inform listeners of event
				forwardEventUpdateAvailableRanges(d.getUid(), d.available);
			}
		}
	}

	/* (non-Javadoc)
	 * @see hydrap2p.download.Downloader.Iface#setSourceDownloadedRange(long, hydrap2p.download.Source, hydrap2p.library.Range)
	 */
	@Override
	public void setSourceDownloadedRange(long uid, Source source, Range oldRange) throws TException {
		Download d = getImplDownloadWithUid(uid);
		
		if(d != null) {
			Source s = getSource(uid, source);

			if(s != null) {
				synchronized(s) {
					s.addToDownloaded(oldRange);
				}
				synchronized(d) {
					d.addDownloadedRange(oldRange);
				}
				
				//Inform listeners of event
				forwardEventUpdateDownloadedRange(d.getUid(), d.downloaded);
			}
		}
	}

	/* (non-Javadoc)
	 * @see hydrap2p.download.Downloader.Iface#setSourceMapExtra(long, hydrap2p.download.Source, java.lang.String, java.lang.String)
	 */
	@Override
	public void setSourceMapExtra(long uid, Source source, String key, String value) throws TException {
		Download d = getImplDownloadWithUid(uid);
		
		if(d != null) {
			Source s = getSource(uid, source);
	
			if(s != null) {
				Map<String, String> map = s.getExtra();
				map.put(key, value);
		
				s.setExtra(map);
				
				//Inform listeners of event
				forwardEventUpdateSource(d.getUid(), s);
			}
		}
	}

	/* (non-Javadoc)
	 * @see hydrap2p.download.Downloader.Iface#setSourceStatus(long, hydrap2p.download.Source, java.lang.String)
	 */
	@Override
	public void setSourceStatus(long uid, Source source, String status) throws TException {
		Download d = getImplDownloadWithUid(uid);
		
		if(d != null) {
			Source s = getSource(uid, source);
			
			if(s != null) {
				s.setStatus(status);
		
				//Inform listeners of event
				forwardEventUpdateSource(d.getUid(), s);
			}
		}
	}

	/* (non-Javadoc)
	 * @see hydrap2p.download.Downloader.Iface#subscribeListener(hydrap2p.ServiceLocation, hydrap2p.download.DownloadSubscriptionType)
	 */
	@Override
	public void subscribeListener(ServiceLocation location, DownloadSubscriptionType subscriptionType) throws TException {
		DownloadSubscriber service = new DownloadSubscriber(location, subscriptionType);

		List<String> interfaces = null;
		try {
			TTransport transport = new TSocket(service.getLocation().getAddress(), service.getLocation().getPort());
			transport.open();
			service.connection = new DownloadListener.Client(new TBinaryProtocol(transport));
			interfaces = service.connection.supportedInterfaces();

		} catch(TException e) {
			// An error happened while connecting to this listener, guess he
			// doesn't really want to listen
			log(Level.ERROR, "TException attempting to connect to listener!");
			return;
		}

		// Ensure it is a DownloadListener
		if(!interfaces.contains("DownloadListener")) {
			log(Level.MISHAP, "Cannot add subscriber which does not implement DownloadListener!");
			log(Level.MISHAP, "Failed to add subscriber at " + location.getAddress() + ":" + location.getPort());
			return;
		}

		synchronized(subscribers) {
			if(subscribers.contains(service)) {
				// Re-add service to update subscription type.
				subscribers.remove(service);
				subscribers.add(service);
			} else
				subscribers.add(service);
		}
		
		log(Level.DEBUG, "Adding subscriber at " + location.getAddress() + ":" + location.getPort());
	}


	/* (non-Javadoc)
	 * @see hydrap2p.download.Downloader.Iface#unsubscribeListener(hydrap2p.ServiceLocation)
	 */
	@Override
	public void unsubscribeListener(ServiceLocation location) throws TException {
		DownloadSubscriber service = new DownloadSubscriber(location, null);
		boolean result;
		
		// If service with the same location exists, remove it
		synchronized(subscribers) {
			result = subscribers.remove(service);
		}
		
		if(result)
			log(Level.DEBUG, "Removing subscriber at " + location.getAddress() + ":" + location.getPort());
		else
			log(Level.MISHAP, "Failed to remove subscriber at " + location.getAddress() + ":" + location.getPort());
	}


	/* (non-Javadoc)
	 * @see hydrap2p.Service.Iface#ping()
	 */
	@Override
	public void ping() throws TException {
//		log(Level.VERBOSE, "Goat successfully teleported!");
	}

	/* (non-Javadoc)
	 * @see hydrap2p.Service.Iface#supportedInterfaces()
	 */
	@Override
	public List<String> supportedInterfaces() throws TException {
		return Arrays.asList("Downloader");
	}
	
	/**
	 * Gets the downloads.
	 * 
	 * @return the downloads
	 */
	@Override
	public Map<Long, BaseDownload> getDownloads() {
		//You would think Java could cast this itself.  I guess not.
		Map<Long, BaseDownload> retMap = new TreeMap<Long, BaseDownload>();
		
		for(Entry<Long, Download> entry : downloads.entrySet()) {
			retMap.put(entry.getKey(), entry.getValue());
		}
		
		return retMap;
	}

	/* (non-Javadoc)
	 * @see hydrap2p.download.Downloader.Iface#getDownloadWithUid(long)
	 */
	@Override
	public BaseDownload getDownloadWithUid(long uid) {
		Download d = downloads.get(uid);

		if(d == null)
			log(Level.MISHAP, "Failed to find download with uid " + uid);

		return d;
	}
	
	/**
	 * Gets the impl download with uid.
	 * 
	 * @param uid the uid
	 * @return the impl download with uid
	 */
	public Download getImplDownloadWithUid(long uid) {
		return (Download)getDownloadWithUid(uid);
	}

	/**
	 * Gets the source.
	 * 
	 * @param uid the uid
	 * @param source the source
	 * @return the source
	 */
	public Source getSource(long uid, Source source) {
		Download d = getImplDownloadWithUid(uid);
		Source retSource = null;
		
		if(d != null) {
			List<Source> sources = d.getSources();
			retSource = sources.get(sources.indexOf(source));
		}

		return retSource;
	}

	/**
	 * Gets the protocol by type.
	 * 
	 * @param arg1 the arg1
	 * @return the protocol by type
	 */
	public ProtocolAdapter getProtocolByType(String type) {
		return protocols.get(type);
	}

	/**
	 * Gets the port.
	 * 
	 * @return the port
	 */
	public int getPort() {
		return port;
	}
	
	/**
	 * Log.
	 * 
	 * @param severity the severity
	 * @param logMessage the log message
	 */
	public void log(Level severity, String logMessage) {
		Logging.Client logging = ServiceConnector.getLogger();
		
		//Log to Logging module if possible, and fall back to console.
		try {
			logging.log(severity, TAG.toString(), logMessage);
		} catch(TException e) {
			logToSystemError(severity, logMessage);
		} catch(NullPointerException e) {
			logToSystemError(severity, logMessage);
		}
	}
	
	/**
	 * Log to system error.
	 * 
	 * @param severity the severity
	 * @param logMessage the log message
	 */
	private void logToSystemError(Level severity, String logMessage) {
		//Log to system error.  If they can't be arsed to use a Logging constant as severity, the message gets logged at trace level
		switch(severity) {
		case VERBOSE:
			consoleLogger.trace(TAG, logMessage);
			break;
		case DEBUG:
			consoleLogger.debug(TAG, logMessage);
			break;
		case INFO:
			consoleLogger.info(TAG, logMessage);
			break;
		case MISHAP:
			consoleLogger.warn(TAG, logMessage);
			break;
		case ERROR:
			consoleLogger.error(TAG, logMessage);
			break;
		default:
			consoleLogger.trace(TAG, logMessage);
		}
	}

	/* (non-Javadoc)
	 * @see hydrap2p.manager.ModuleControl.Iface#stop()
	 */
	@Override
	public void stop() throws TException {
		consoleLogger.info(TAG, "Stopping downloader server");
		
		consoleLogger.info(TAG, "Download map size: " + downloads.size());
		
		consoleLogger.info(TAG, "Saving state...");
		boolean result = DownloaderPersistence.saveAllDownloads(downloads, savedDownloads);
		consoleLogger.info(TAG, "Download save result: " + result);
		
		t.shutdown();
		control.shutdown();
	}

	/* (non-Javadoc)
	 * @see hydrap2p.download.Downloader.Iface#removeDownload(long)
	 */
	@Override
	public boolean removeDownload(long uid) throws TException {
		log(Level.DEBUG, "Removing download with UID " + uid);
		
		Download d = downloads.remove(uid);
		forwardEventRemovedDownload(uid);
		
		//TODO: Kill in-progress download calls
		if(d != null) {
			for(Source s : d.getSources()) {
				ProtocolAdapter pa = getProtocolByType(s.getProtocol());
				//TODO: Make sure this works
				pa.cancelDownload(uid, s);
			}
		}
		
		return d != null;
	}
	
	//Event forwarding entry points
	/**
	 * Forward event new source.
	 * 
	 * @param d the d
	 * @param source the source
	 */
	public void forwardEventNewSource(Download d, Source source) {
		synchronized(subscribers) {
			List<DownloadSubscriber> failed = eventFwd.forwardEventNewSource(subscribers, d, source);
			subscribers.removeAll(failed);
		}
	}

	/**
	 * Forward event new download.
	 * 
	 * @param uid the uid
	 */
	public void forwardEventNewDownload(long uid) {
		synchronized(subscribers) {
			List<DownloadSubscriber> failed = eventFwd.forwardEventNewDownload(subscribers, uid);
			subscribers.removeAll(failed);
		}
	}
	
	/**
	 * Forward event removed download.
	 * 
	 * @param uid the uid
	 */
	public void forwardEventRemovedDownload(long uid) {
		synchronized(subscribers) {
			List<DownloadSubscriber> failed = eventFwd.forwardEventRemovedDownload(subscribers, uid);
			subscribers.removeAll(failed);
		}
	}

	/**
	 * Forward event update download state.
	 * 
	 * @param uid the uid
	 * @param state the state
	 */
	public void forwardEventUpdateDownloadState(long uid, DownloadState state) {
		synchronized(subscribers) {
			List<DownloadSubscriber> failed = eventFwd.forwardEventUpdateDownloadState(subscribers, uid, state);
			subscribers.removeAll(failed);
		}
	}

	/**
	 * Forward event removed source.
	 * 
	 * @param uid the uid
	 * @param source the source
	 */
	public void forwardEventRemovedSource(long uid, Source source) {
		synchronized(subscribers) {
			List<DownloadSubscriber> failed = eventFwd.forwardEventRemovedSource(subscribers, uid, source);
			subscribers.removeAll(failed);
		}
	}

	/**
	 * Forward event update source.
	 * 
	 * @param uid the uid
	 * @param source the source
	 */
	public void forwardEventUpdateSource(long uid, Source source) {
		synchronized(subscribers) {
			List<DownloadSubscriber> failed = eventFwd.forwardEventUpdateSource(subscribers, uid, source);
			subscribers.removeAll(failed);
		}
	}

	/**
	 * Forward event update downloaded range.
	 * 
	 * @param uid the uid
	 * @param list the list
	 */
	public void forwardEventUpdateDownloadedRange(long uid, List<Range> list) {
		synchronized(subscribers) {
			List<DownloadSubscriber> failed = eventFwd.forwardEventUpdateDownloadedRange(subscribers, uid, list);
			subscribers.removeAll(failed);
		}	
	}

	/**
	 * Forward event update available ranges.
	 * 
	 * @param uid the uid
	 * @param list the list
	 */
	public void forwardEventUpdateAvailableRanges(long uid, List<Range> list) {
		synchronized(subscribers) {
			List<DownloadSubscriber> failed = eventFwd.forwardEventUpdateAvailableRanges(subscribers, uid, list);
			subscribers.removeAll(failed);
		}	
	}

	/**
	 * Forward event update verified ranges.
	 * 
	 * @param uid the uid
	 * @param list the list
	 */
	public void forwardEventUpdateVerifiedRanges(long uid, List<Range> list) {
		synchronized(subscribers) {
			List<DownloadSubscriber> failed = eventFwd.forwardEventUpdateVerifiedRanges(subscribers, uid, list);
			subscribers.removeAll(failed);
		}
	}

	/**
	 * Forward event update download progress.
	 * 
	 * @param uid the uid
	 * @param downloaded the downloaded
	 * @param total the total
	 */
	public void forwardEventUpdateDownloadProgress(long uid, Source source, long downloaded, long total) {
		synchronized(subscribers) {
			List<DownloadSubscriber> failed = eventFwd.forwardEventUpdateDownloadProgress(subscribers, uid, source, downloaded, total);
			subscribers.removeAll(failed);
		}
	}

	/**
	 * Forward event update verified progress.
	 * 
	 * @param uid the uid
	 * @param verified the verified
	 * @param total the total
	 */
	public void forwardEventUpdateVerifiedProgress(long uid, long verified, long total) {
		synchronized(subscribers) {
			List<DownloadSubscriber> failed = eventFwd.forwardEventUpdateVerifiedProgress(subscribers, uid, verified, total);
			subscribers.removeAll(failed);
		}
	}

	@Override
	public void triggerDownloadEvent(DownloadEvent eventType) throws TException {
		// TODO Auto-generated method stub
		
	}
	
	@Override
	public boolean setDownloadExtra(long uid, String key, String value) {
		Download d = getImplDownloadWithUid(uid);
		
		if(d != null) {
			d.getExtra().put(key, value);
			
			return true;
		}
		
		return false;
	}
}